/// ===========================================================================
/// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
/// KIND, WHETHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
/// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
/// PURPOSE.
/// ===========================================================================
/// 
/// Project:        SharePoint Faceted Search
/// Author:         Leonid Lyublinski (leonidly@microsoft.com)
/// Company:        Microsoft Services
/// Date:           05/04/2007  Version:        1.0
///
/// ===========================================================================
/// $History: $
/// 
/// ===========================================================================
/// 
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Xml.Serialization;
using System.Data;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Search.Administration;
using Microsoft.Office.Server.Search.Query;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace Microsoft.SharePoint.Portal.ExtendedSearch.WebControls
{
    /// <summary>
    /// Custom faceted search web part.
    /// The web part has a series of shared web part properties that can be used
    /// to control the query visually and functionally.
    /// </summary>
    [ToolboxData("<{0}:SearchFacets runat=server></{0}:SearchFacets>")]
    [XmlRoot(Namespace = "Microsoft.SharePoint.Portal.ExtendedSearch")]
    public class SearchFacets : System.Web.UI.WebControls.WebParts.WebPart
    {
        #region Constants
        internal const string ERROR_LOADING_XML = "Error loading facets as Xml. Check value of \"Select Columns for facets property\".";
        internal const string ERROR_CREATING_CONTROLS = "Error creating child controls.";
        internal const string ERROR_LOADING_FACETS = "Managed properties don't contain the facet: ";
        internal const string ERROR_CONVERTING_ORDERBY = "The OrderBy in XML property assigned an invalid value.";
        internal const string ERROR_CONVERTING_MAXRESULTS = "The MaxResults in XML property assigned an invalid value.";
        #endregion

        #region Fields
        private short _resultsPerPage;// = 100;
        private string _selectColumns;// = @"<root xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""><Columns><Column Name=""Path"" /></Columns></root>";
        private XmlDocument _xmlColumns;
        private Dictionary<string, Dictionary<string, Int32>> _facets;

        private string _scope;
        private string _k;
        private string _sql;
        private int _start;
        private string _v;
        private NameValueCollection _properties;

        private string _facetNameCss;// = "ms-navheader";
        private string _facetNameContainerCss;// = "ms-quicklaunch";
        private string _facetValueCss;// = "ms-navitem";
        private string _facetValueContainerCss;// = "ms-navSubMenu2";
        private string _listCss;// = "ms-navsubmenu1";
        private string _listFacetsCss;// = "ms-navsubmenu1";
        private short _cropMax;// = 20;

        private Dictionary<string, int> _facetTables = new Dictionary<string, int>();
        #endregion

        #region Properties
        /// <summary>
        /// Number of results returned by the search engine in one resultset.
        /// The higher value, the more accurate is the calculation of facet hits.
        /// </summary>
        /// <remarks>High value will result in performance penalty.</remarks>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Total Results")]
        [Description("Number of results returned by the search to selectively define facets.")]
        public short ResultsPerPage
        {
            get
            {
                return _resultsPerPage;
            }
            set
            {
                _resultsPerPage = value;
            }
        }

        /// <summary>
        /// Selected columns are defined as managed properties and will be available as search facets. 
        /// The columns must be crawled and set as managed properties in the SSP administration.
        /// </summary>
        /// <remarks>Test against existing managed properties is built-in, however a proper exception propagation is still required.</remarks>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Select Columns for Facets")]
        [Description("Selected columns should be defined as managed properties BEFORE. They will get available as search facets")]
        public string SelectColumns
        {
            get
            {
                return _selectColumns;
            }
            set
            {
                _selectColumns = value;
                SearchContext context = SearchContext.GetContext(ServerContext.Current);
                Schema schema = new Schema(context);
                ManagedPropertyCollection properties = schema.AllManagedProperties;
                StringCollection facets = GetFacets();
                foreach (string facet in facets)
                {
                    if (!properties.Contains(facet))
                    {
                        // do something
                        //throw new ApplicationException(string.Concat(ERROR_LOADING_FACETS, facet));
                    }
                }
            }
        }

        /// <summary>
        /// The CSS class for the list of facets.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("List CSS Class")]
        [Description("Select CSS class for the entire list.")]
        public string ListCss
        {
            get
            {
                return _listCss;
            }
            set
            {
                _listCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face name.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet Name Container CSS Class")]
        [Description("Select CSS class for the facet header container .")]
        public string FacetNameContainerCss
        {
            get
            {
                return _facetNameContainerCss;
            }
            set
            {
                _facetNameContainerCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face name.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet CSS Class")]
        [Description("Select CSS class for the facet header.")]
        public string FacetNameCss
        {
            get
            {
                return _facetNameCss;
            }
            set
            {
                _facetNameCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the list of facets.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facets List CSS Class")]
        [Description("Select CSS class for the entire list of facets.")]
        public string FacetsListCss
        {
            get
            {
                return _listFacetsCss;
            }
            set
            {
                _listFacetsCss = value;
            }
        }
        /// <summary>
        /// The CSS class for the face value.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet Value Container CSS Class")]
        [Description("Select CSS class for the facet value container .")]
        public string FacetValueContainerCss
        {
            get
            {
                return _facetValueContainerCss;
            }
            set
            {
                _facetValueContainerCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face value and # of hits.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Hit CSS Class")]
        [Description("Select CSS class for the facet value.")]
        public string FacetValueCss
        {
            get
            {
                return _facetValueCss;
            }
            set
            {
                _facetValueCss = value;
            }
        }


        /// <summary>
        /// Maximum number of characters in a facet before truncation. After the facet value reached the maximum, 
        /// it is truncated, "..." aded to the end. Also, ToolTip is added for long values with a full text.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet Length")]
        [Description("Maximum number of characters in a facet before truncation.")]
        public short CropMax
        {
            get
            {
                return _cropMax;
            }
            set
            {
                _cropMax = value;
            }
        }

        #endregion

        #region Delegates
        /// <summary>
        /// The method fires for each bound DataList item and dynamically builds its content.
        /// Building of the content is done via population of Controls collection.
        /// </summary>
        /// <param name="sender">DataList used as a placeholder for facets.</param>
        /// <param name="e"><typeparamref name="DataListItemEventArgs"/></param>
        /// <remarks>Each DataList item represents one Facet, rendered as a label and a set of Facet Values, rendered as a datalist.</remarks>
        protected void rpt_ItemDataBound(object sender, DataListItemEventArgs e)
        {
            DataListItem item = e.Item;
            DataList list = new DataList();

            FacetCollection facetCollection = (FacetCollection)item.DataItem;
            list.ID = facetCollection.Name;
            list.Attributes.Add("facet", facetCollection.Name);
            list.ItemCommand += new DataListCommandEventHandler(list_ItemCommand);
            list.ShowFooter = false;
            list.ShowHeader = true;

            // facet name
            list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), -1);
            list.HeaderStyle.Width = Unit.Percentage(100);
            list.HeaderStyle.CssClass = _facetNameContainerCss;
            // facet value
            list.ItemTemplate = new FacetTemplate(ListItemType.Item, _facetValueCss, _cropMax);
            list.ItemStyle.Width = Unit.Percentage(100);
            list.ItemStyle.CssClass = _facetValueContainerCss;


            // list.CssClass = _listCss;
            list.Width = Unit.Percentage(100);
            list.DataSource = facetCollection;

            // adding sorting
            string orderBy = GetColumnAttribute(facetCollection.Name, "OrderBy");
            if (!string.IsNullOrEmpty(orderBy))
            {
                try
                {
                    FacetSortEnum facetOrderBy = FacetSortEnum.None;
                    EnumConverter converter = (EnumConverter)TypeDescriptor.GetConverter(facetOrderBy);
                    facetOrderBy = (FacetSortEnum)converter.ConvertFromString(orderBy);
                    facetCollection.SortOrder = facetOrderBy;
                    facetCollection.Sort(facetCollection);
                }
                catch
                {
                    throw new NotSupportedException(ERROR_CONVERTING_ORDERBY);
                }
            }

            // adding paging
            string maxResults = GetColumnAttribute(facetCollection.Name, "TotalResults");
            if (!string.IsNullOrEmpty(maxResults))
            {
                int totalResults;
                if (int.TryParse(maxResults, out totalResults))
                {
                    if (totalResults > -1 && facetCollection.Count > totalResults) // means all results
                    {
                        list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), totalResults);

                        // review styles if new web part properties needed
                        list.FooterTemplate = new FooterTemplate(ListItemType.Footer, _facetValueCss, list.ID, totalResults);
                        list.FooterStyle.Width = Unit.Percentage(100);
                        list.FooterStyle.CssClass = _facetValueContainerCss;
                        list.ShowFooter = true;

                        _facetTables.Add(list.ID, totalResults);
                    }
                }
            }

            list.DataBind();
            item.Controls.Add(list);


            //item.Controls.Add(new LiteralControl("<BR>"));
        }

        void list_ItemDataBound(object sender, DataListItemEventArgs e)
        {
            DataListItem item = e.Item;
            int i = item.ItemIndex;
            DataList list = (DataList)sender;
            string id = list.ID;
            string totalResults = GetColumnAttribute(list.ID, "TotalResults");
            if (!string.IsNullOrEmpty(totalResults))
            {
                int totalResultsInt = int.Parse(totalResults);
                if (i > totalResultsInt)
                    item.Attributes.Add("visible", "true");
                else
                    item.Attributes.Add("visible", "false");
            }

        }

        /// <summary>
        /// The method fires when the Facet link clicked in the facet menu. Functionality is repsonsible for refining the search results.
        /// The code parses Facet and its value, reconstructs the querystring and redirects the user to a constructed query.
        /// </summary>
        /// <param name="source">DataList</param>
        /// <param name="e"><typeparamref name="DataListCommandEventArgs"/></param>
        /// <remarks>Currently implemented as a postback. Consider redoing as apure hyperlink instead.</remarks>
        protected void list_ItemCommand(object source, DataListCommandEventArgs e)
        {
            if (e.CommandName == "SelectFacet")
            {
                DataList list = (DataList)source;
                string facetName = list.ID;
                LinkButton link = (LinkButton)e.CommandSource;
                // account for truncation
                string facetValue = (string.IsNullOrEmpty(link.ToolTip)) ? link.Text : link.ToolTip;
                int hits = int.Parse(link.CommandArgument);
                if (_properties[facetName] == null)
                    _properties.Add(facetName, facetValue);
                else
                    _properties[facetName] = facetValue;

                RefineSearch();
            }
            //throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// Constructs the Controls collection of the webpart.
        /// </summary>
        /// <remarks>Consider test against Page.IsPostBack event along with stateful controls. Currently is not implemented</remarks>
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            try
            {
                if (Context.Request.QueryString != null)
                {
                    SearchQuery query = Utility.ExtractQuery();
                    _k = query.Keywords;
                    _scope = query.Scope;
                    _start = query.Start;
                    _v = query.View;
                    _properties = query.Properties;
                    BuildSQL();
                }

                DataTable dt = GetResults();
                BuildFacets(dt);

                DataList listFacets = new DataList();
                listFacets.ItemDataBound += new DataListItemEventHandler(rpt_ItemDataBound);
                listFacets.CssClass = _listCss;
                FacetMenu facetMenu = new FacetMenu(_facets);
                // adding sorting
                string orderBy = GetColumnsAttribute("OrderBy");
                if (!string.IsNullOrEmpty(orderBy))
                {
                    try
                    {
                        FacetSortEnum facetsOrderBy = FacetSortEnum.None;
                        EnumConverter converter = (EnumConverter)TypeDescriptor.GetConverter(facetsOrderBy);
                        facetsOrderBy = (FacetSortEnum)converter.ConvertFromString(orderBy);
                        facetMenu.SortOrder = facetsOrderBy;
                        facetMenu.Sort(facetMenu);
                    }
                    catch
                    {
                        throw new NotSupportedException(ERROR_CONVERTING_ORDERBY);
                    }
                }

                listFacets.DataSource = facetMenu;
                listFacets.DataBind();
                Controls.Add(listFacets);
            }
            catch (Exception ex)
            {
                Label error = new Label();
                error.EnableViewState = false;
                error.Text = ex.Message;
                error.CssClass = "ms-error";
                Controls.Add(error);
                //throw new ArgumentException(ERROR_CREATING_CONTROLS, ex);
            }
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            InitClientScripts();
        }

        protected override void OnPreRender(EventArgs e)
        {
            // put StringBuilder
            base.OnPreRender(e);
            string arFacetTables = "";
            string arTotalResults = "";
            foreach (KeyValuePair<string, int> pair in _facetTables)
            {
                arFacetTables += ",'" + pair.Key + "'";
                arTotalResults += ",'" + pair.Value.ToString() + "'";
            }

            ClientScriptManager csm = Page.ClientScript;
            csm.RegisterArrayDeclaration("arFacetTables", arFacetTables.Trim(','));
            csm.RegisterArrayDeclaration("arTotalResults", arTotalResults.Trim(','));

            csm.RegisterStartupScript(this.GetType(), "Faceted Search Startup", "document.body.onload = initFacetTables;", true);
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Builds a search string and redirects request using a built querystring.
        /// </summary>
        private void RefineSearch()
        {
            SearchQuery query = new SearchQuery();
            query.Keywords = _k;
            query.Scope = _scope;
            query.Start = _start;
            query.View = _v;
            query.Properties = _properties;

            string qs = Utility.BuildQueryString(query);
            string qsEncoded = string.Format("{0}?{1}", Page.Request.Url.AbsolutePath, qs);
            Page.Response.Redirect(qsEncoded); // consider use of HyperLinks w/o posts
        }

        /// <summary>
        /// Processes received search results and build a facet structure with totals.
        /// </summary>
        /// <param name="dt"></param>
        private void BuildFacets(DataTable dt)
        {
            // our SELECT statement already lists all facets as columns.
            // no need to check, just skip column #1 Path
            StringCollection facets = GetFacets();
            Dictionary<string, Dictionary<string, Int32>> facetStructure = new Dictionary<string, Dictionary<string, Int32>>(facets.Count);

            foreach (DataRow row in dt.Rows)
            { // process each result
                for (int i = 1; i < dt.Columns.Count; i++) // process each meta column except Path
                {
                    string facetName = dt.Columns[i].ColumnName;
                    if (row[facetName] is string[]) // found multiple values!!!
                    {
                        string[] facetValues = (string[])row[facetName];
                        foreach (string facetValue in facetValues)
                        {
                            AddFacet(facetStructure, facetName, facetValue);
                        }
                    }
                    else
                    {
                        // prepare for lookup column with multiple selection
                        Regex re = new Regex(@";#\d+;#", RegexOptions.Singleline);

                        string facetValue = row[i].ToString();
                        if (re.IsMatch(facetValue))
                        {
                            string[] facetValues = re.Split(facetValue);
                            foreach (string lookup in facetValues)
                            {
                                AddFacet(facetStructure, facetName, lookup);
                            }
                        }
                        else
                        {
                            AddFacet(facetStructure, facetName, facetValue);
                        }
                    }
                }
            }
            //remove empty facets so they don't show up in the facet menu
            _facets = facetStructure;
        }

        /// <summary>
        /// Helper method to populate facet structure.
        /// </summary>
        /// <param name="facetStructure"></param>
        /// <param name="facetName"></param>
        /// <param name="facetValue"></param>
        private void AddFacet(Dictionary<string, Dictionary<string, Int32>> facetStructure, string facetName, string facetValue)
        {
            if (String.IsNullOrEmpty(facetName)) return;
            if (String.IsNullOrEmpty(facetValue)) return;

            if (!facetStructure.ContainsKey(facetName)) facetStructure.Add(facetName, new Dictionary<string, int>());
            Dictionary<string, Int32> facetCounter = facetStructure[facetName];
            if (!facetCounter.ContainsKey(facetValue))
            {
                facetCounter.Add(facetValue, 1);
            }
            else
            {
                facetCounter[facetValue] += 1;
            }
        }

        /// <summary>
        /// Executes FullTextSqlQuery with constructed earlier SQL statement and returns
        /// DataTable with all Relevant results.
        /// </summary>
        /// <returns></returns>
        private DataTable GetResults()
        {
            ResultTableCollection rtc;

            using (FullTextSqlQuery query = new FullTextSqlQuery(ServerContext.Current))
            {
                query.EnableStemming = true;
                query.RowLimit = this._resultsPerPage;
                query.StartRow = 0;
                query.SiteContext = new Uri(SPControl.GetContextSite(this.Context).Url);
                query.TrimDuplicates = true;
                query.EnableStemming = true;
                query.IgnoreAllNoiseQuery = true;
                query.AuthenticationType = QueryAuthenticationType.NtAuthenticatedQuery;
                if (SPSecurity.AuthenticationMode != System.Web.Configuration.AuthenticationMode.Windows)
                {
                    query.AuthenticationType = QueryAuthenticationType.PluggableAuthenticatedQuery;
                }
                query.ResultTypes |= ResultType.RelevantResults;
                query.QueryText = _sql;
                //execute the query using the query object
                rtc = query.Execute();
            }

            DataTable dt = new DataTable();
            if ((rtc != null) && rtc.Exists(ResultType.RelevantResults))
            {
                ResultTable rt = rtc[ResultType.RelevantResults];
                if (rt.RowCount > 0)
                {
                    dt.TableName = rt.ResultType.ToString();
                    dt.Load(rt, LoadOption.OverwriteChanges);
                }
            }
            //TestFacets(dt);
            return dt;
        }

        #region SQL Statements
        /// <summary>
        /// Returns quoted delimited string of columns, used in building SQL
        /// </summary>
        /// <returns></returns>
        private string GetParsedColumns()
        {
            StringCollection columns = GetFacets();
            string columnsStr = string.Empty;
            if (columns.Count > 0)
            {
                ArrayList al = new ArrayList(columns);
                columnsStr = String.Join("\", \"", (string[])al.ToArray(typeof(string)));
                columnsStr = "\"" + columnsStr + "\"";
            }
            return columnsStr;
        }

        /// <summary>
        /// Constructs SELECT SQL statement from the querystring based on defined 
        /// facets through the WP UI.
        /// </summary>
        /// <returns></returns>
        private string GetSELECT()
        {
            string select = " SELECT Path ";
            string columns = GetParsedColumns();
            if (!columns.Equals(string.Empty))
                select = select + ", " + columns;

            return select;
        }

        private string GetFROM()
        {
            return " FROM scope() ";
        }

        private string GetWHERE()
        {
            ArrayList where = new ArrayList();
            if (!string.IsNullOrEmpty(_k))
                //where.Add(" CONTAINS('\"" + _k + "*\"') "); // doesn't conform core results totals
                where.Add(" CONTAINS('\"" + _k + "\"') ");
            if (!string.IsNullOrEmpty(_scope))
                where.Add("\"scope\"='" + _scope + "' ");

            for (int i = 0; i < _properties.Count; i++)
            {
                string propName = _properties.Keys[i];
                string propValue = _properties[propName];
                where.Add(" CONTAINS(\"" + propName + "\", '\"" + propValue + "\"') "); // no wild card here
            }




            if (where.Count > 0)
                return " WHERE " + string.Join(" AND ", (string[])where.ToArray(typeof(string)));
            return
                string.Empty;
        }

        private string GetORDERBY()
        {
            string orderby = " ORDER BY \"Rank\" DESC ";
            string columns = GetParsedColumns();
            if (!columns.Equals(string.Empty))
                orderby = orderby + ", " + columns;
            return String.Empty; //for now
        }

        /// <summary>
        /// Constructs final SQL statement from the querystring
        /// </summary>
        private void BuildSQL()
        {
            _sql = GetSELECT();
            _sql += GetFROM();
            _sql += GetWHERE();
            _sql += GetORDERBY();
            /******* code below to show SQL clause in UI *****/
            /**
            Label sql = new Label();
            sql.Text = _sql;
            Controls.Add(sql);
             * */
        }
        #endregion

        /// <summary>
        /// Returns collection of columns defined as facets in the webpart UI
        /// </summary>
        /// <remarks>add validation against managed properties</remarks>
        /// <returns></returns>
        private StringCollection GetFacets()
        {

            StringCollection selectedFacetsCollection = new StringCollection();

            //Get selected columns
            _xmlColumns = new XmlDocument();
            try
            {
                _xmlColumns.LoadXml(SelectColumns);
                XmlNodeList selectedFacets = _xmlColumns.GetElementsByTagName("Column");
                //Populate column name collection
                foreach (XmlElement column in selectedFacets)
                {
                    string name = column.GetAttribute("Name");
                    selectedFacetsCollection.Add(name);
                }
            }
            catch (XmlException ex)
            {
                throw new ApplicationException(ERROR_LOADING_XML, ex);
            }
            return selectedFacetsCollection;
        }

        /// <summary>
        /// Returns value of the Column attribute in XML string set by SelectColumns property
        /// </summary>
        /// <param name="attName">name of the attribute</param>
        /// <param name="columnName">name of the column</param>
        /// <returns></returns>
        private string GetColumnAttribute(string columnName, string attName)
        {
            try
            {
                // building smth like \\Column[@Name='Product']@OrderBy
                string xpath = string.Format("//Column[@Name='{0}']/@{1}", columnName, attName);
                XmlNode node = _xmlColumns.SelectSingleNode(xpath);
                if (node == null) return null;
                else return node.Value;
            }
            catch (XPathException ex)
            {
                return null;
            }
        }

        /// <summary>
        /// Returns value of the Columns attribute in XML string set by SelectColumns property
        /// </summary>
        /// <param name="attName">name of the attribute</param>
        /// <returns></returns>
        private string GetColumnsAttribute(string attName)
        {
            try
            {
                XmlNode columns = _xmlColumns.DocumentElement.ChildNodes[0];
                if (columns.Attributes[attName] != null) return columns.Attributes[attName].Value;
                return null;
            }
            catch (XPathException ex)
            {
                return null;
            }
        }

        /// <summary>
        /// A test method used to display Grid with search results for testing
        /// </summary>
        /// <param name="dt"></param>
        private void TestFacets(DataTable dt)
        {
            GridView view = new GridView();
            view.DataSource = dt;
            view.DataBind();
            Controls.Add(view);
        }

        private void InitClientScripts()
        {
            string key = "FacetedSearch";
            ClientScriptManager scriptManager = Page.ClientScript;
            if (!scriptManager.IsClientScriptBlockRegistered(key))
            {
                string script = @";
function switchFooter(obj, name, totalResults) {
    try {
        var table = getFacetTableByObj(obj, name);
        if (table) {
            var re = /DLMIN.gif/;
            var img = obj.children[0];
            var expanded = re.test(img.src); 
            if (expanded){ // then collapse
                img.src = img.src.replace(/DLMIN.gif/, ""DLMAX.gif"");
            } else {
                img.src = img.src.replace(/DLMAX.gif/, ""DLMIN.gif"");
            }   
            
            setFacetTable(table, expanded, totalResults);
        }
    } catch (e) {}
    finally {return false;}
}

function setFacetTable(table, expanded, totalResults){
    var total = table.rows.length;     
    if (expanded){// then collapse and show last row with ...
        table.rows[total - 1].style.display = ""block"";  
    } else {
        table.rows[total - 1].style.display = ""none"";  
    }   

    for(var i = parseInt(totalResults) + 1; i<total-1; i++) {
        var row = table.rows[i];
        if (expanded){// then collapse and hide all rows
            row.style.display = ""none"";
        } else {
            row.style.display = ""block"";
        }
    }
}

function getFacetTableByObj(obj, name) {
    var parent = obj.parentElement;
    while ((parent != null) && (parent.tagName != ""Table"")) {
        parent = parent.parentElement;
        if (parent.getAttribute(""facet"") == name){
            return parent;
        }
    }
}

function getFacetTableByName(name) {
    var tables = document.body.getElementsByTagName(""Table"");
    for (var i=0; i<tables.length; i++) {
        var table = tables[i];
        if (table.getAttribute(""facet"") == name){
            return table;
        }
    }
}

function initFacetTables(){
    if (arFacetTables){
        for (var i=0; i<arFacetTables.length; i++) {
            var name = arFacetTables[i];
            var totalResults = arTotalResults[i];
            var table = getFacetTableByName(name);
            setFacetTable(table, true, totalResults)
        }
    }
}
                ";
                scriptManager.RegisterClientScriptBlock(this.GetType(), key, script, true);
            }
        }
        #endregion

    }

}
